package com.swak.selector;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.util.Assert;

/**
 * 用于 配置项 的排序
 *
 * @author: lifeng
 * @date: 2020/4/1 12:37
 */
public class AutoConfigurationSorter {

    private final MetadataReaderFactory metadataReaderFactory;

    AutoConfigurationSorter(MetadataReaderFactory metadataReaderFactory) {
        Assert.notNull(metadataReaderFactory, "MetadataReaderFactory must not be null");
        this.metadataReaderFactory = metadataReaderFactory;
    }

    public List<String> getInPriorityOrder(Collection<String> classNames) {
        AutoConfigurationClasses classes = new AutoConfigurationClasses(this.metadataReaderFactory, classNames);
        List<String> orderedClassNames = new ArrayList<>(classNames);
        // Initially sort alphabetically
        Collections.sort(orderedClassNames);
        // Then sort by order
        orderedClassNames.sort((o1, o2) -> {
            int i1 = classes.get(o1).getOrder();
            int i2 = classes.get(o2).getOrder();
            return Integer.compare(i1, i2);
        });
        // Then respect @AutoConfigureBefore @AutoConfigureAfter
        orderedClassNames = sortByAnnotation(classes, orderedClassNames);
        return orderedClassNames;
    }

    private List<String> sortByAnnotation(AutoConfigurationClasses classes, List<String> classNames) {
        List<String> toSort = new ArrayList<>(classNames);
        toSort.addAll(classes.getAllNames());
        Set<String> sorted = new LinkedHashSet<>();
        Set<String> processing = new LinkedHashSet<>();
        while (!toSort.isEmpty()) {
            doSortByAfterAnnotation(classes, toSort, sorted, processing, null);
        }
        sorted.retainAll(classNames);
        return new ArrayList<>(sorted);
    }

    private void doSortByAfterAnnotation(AutoConfigurationClasses classes, List<String> toSort, Set<String> sorted,
                                         Set<String> processing, String current) {
        if (current == null) {
            current = toSort.remove(0);
        }
        processing.add(current);
        for (String after : classes.getClassesRequestedAfter(current)) {
            Assert.state(!processing.contains(after),
                    "AutoConfigure cycle detected between " + current + " and " + after);
            if (!sorted.contains(after) && toSort.contains(after)) {
                doSortByAfterAnnotation(classes, toSort, sorted, processing, after);
            }
        }
        processing.remove(current);
        sorted.add(current);
    }

    private static class AutoConfigurationClasses {

        private final Map<String, AutoConfigurationClass> classes = new HashMap<>();

        AutoConfigurationClasses(MetadataReaderFactory metadataReaderFactory, Collection<String> classNames) {
            addToClasses(metadataReaderFactory, classNames, true);
        }

        public Set<String> getAllNames() {
            return this.classes.keySet();
        }

        private void addToClasses(MetadataReaderFactory metadataReaderFactory, Collection<String> classNames,
                                  boolean required) {
            for (String className : classNames) {
                if (!this.classes.containsKey(className)) {
                    AutoConfigurationClass autoConfigurationClass = new AutoConfigurationClass(className,
                            metadataReaderFactory);
                    boolean available = autoConfigurationClass.isAvailable();
                    if (required || available) {
                        this.classes.put(className, autoConfigurationClass);
                    }
                    if (available) {
                        addToClasses(metadataReaderFactory, autoConfigurationClass.getBefore(), false);
                        addToClasses(metadataReaderFactory, autoConfigurationClass.getAfter(), false);
                    }
                }
            }
        }

        public AutoConfigurationClass get(String className) {
            return this.classes.get(className);
        }

        public Set<String> getClassesRequestedAfter(String className) {
			Set<String> classesRequestedAfter = new LinkedHashSet<>(get(className).getAfter());
            this.classes.forEach((name, autoConfigurationClass) -> {
                if (autoConfigurationClass.getBefore().contains(className)) {
                    classesRequestedAfter.add(name);
                }
            });
            return classesRequestedAfter;
        }

    }

    private static class AutoConfigurationClass {

        private final String className;

        private final MetadataReaderFactory metadataReaderFactory;

        private volatile AnnotationMetadata annotationMetadata;

        private volatile Set<String> before;

        private volatile Set<String> after;

        AutoConfigurationClass(String className, MetadataReaderFactory metadataReaderFactory) {
            this.className = className;
            this.metadataReaderFactory = metadataReaderFactory;
        }

        public boolean isAvailable() {
            return true;
        }

        public Set<String> getBefore() {
            if (this.before == null) {
                this.before = getAnnotationValue(AutoConfigureBefore.class);
            }
            return this.before;
        }

        public Set<String> getAfter() {
            if (this.after == null) {
                this.after = getAnnotationValue(AutoConfigureAfter.class);
            }
            return this.after;
        }

        private int getOrder() {
            Map<String, Object> attributes = getAnnotationMetadata()
                    .getAnnotationAttributes(AutoConfigureOrder.class.getName());
            return (attributes == null ? AutoConfigureOrder.DEFAULT_ORDER : (Integer) attributes.get("value"));
        }

        private Set<String> getAnnotationValue(Class<?> annotation) {
            Map<String, Object> attributes = getAnnotationMetadata().getAnnotationAttributes(annotation.getName(),
                    true);
            if (attributes == null) {
                return Collections.emptySet();
            }
            Set<String> value = new LinkedHashSet<>();
            Collections.addAll(value, (String[]) attributes.get("value"));
            Collections.addAll(value, (String[]) attributes.get("name"));
            return value;
        }

        private AnnotationMetadata getAnnotationMetadata() {
            if (this.annotationMetadata == null) {
                try {
                    MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(this.className);
                    this.annotationMetadata = metadataReader.getAnnotationMetadata();
                } catch (IOException ex) {
                    throw new IllegalStateException("Unable to read meta-data for class " + this.className, ex);
                }
            }
            return this.annotationMetadata;
        }
    }
}